/*
 * MIT License
 *
 * Copyright (c) 2023 北京凯特伟业科技有限公司
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.je.bpm.engine.impl.bpmn.behavior;

import com.je.bpm.core.model.*;
import com.je.bpm.engine.ActivitiException;
import com.je.bpm.engine.ProcessEngineConfiguration;
import com.je.bpm.engine.delegate.DelegateExecution;
import com.je.bpm.engine.delegate.Expression;
import com.je.bpm.engine.delegate.event.impl.ActivitiEventBuilder;
import com.je.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl;
import com.je.bpm.engine.impl.context.Context;
import com.je.bpm.engine.impl.delegate.SubProcessActivityBehavior;
import com.je.bpm.engine.impl.el.ExpressionManager;
import com.je.bpm.engine.impl.persistence.entity.ExecutionEntity;
import com.je.bpm.engine.impl.persistence.entity.ExecutionEntityManager;
import com.je.bpm.engine.impl.util.ProcessDefinitionUtil;
import com.je.bpm.engine.repository.ProcessDefinition;
import org.apache.commons.lang3.StringUtils;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.je.bpm.core.model.process.Process;

/**
 * Implementation of the BPMN 2.0 call activity (limited currently to calling a subprocess and not (yet) a global task).
 */
public class CallActivityBehavior extends AbstractBpmnActivityBehavior implements SubProcessActivityBehavior {

    private static final long serialVersionUID = 1L;

    protected String processDefinitonKey;
    protected Expression processDefinitionExpression;
    protected List<MapExceptionEntry> mapExceptions;

    public CallActivityBehavior(String processDefinitionKey, List<MapExceptionEntry> mapExceptions) {
        this.processDefinitonKey = processDefinitionKey;
        this.mapExceptions = mapExceptions;
    }

    public CallActivityBehavior(Expression processDefinitionExpression, List<MapExceptionEntry> mapExceptions) {
        this.processDefinitionExpression = processDefinitionExpression;
        this.mapExceptions = mapExceptions;
    }

    @Override
    public void execute(DelegateExecution execution) {

        String finalProcessDefinitonKey = null;
        if (processDefinitionExpression != null) {
            finalProcessDefinitonKey = (String) processDefinitionExpression.getValue(execution);
        } else {
            finalProcessDefinitonKey = processDefinitonKey;
        }

        ProcessDefinition processDefinition = findProcessDefinition(finalProcessDefinitonKey, execution.getTenantId());

        // Get model from cache
        Process subProcess = ProcessDefinitionUtil.getProcess(processDefinition.getId());
        if (subProcess == null) {
            throw new ActivitiException("Cannot start a sub process instance. Process model " + processDefinition.getName() + " (id = " + processDefinition.getId() + ") could not be found");
        }

        FlowElement initialFlowElement = subProcess.getInitialFlowElement();
        if (initialFlowElement == null) {
            throw new ActivitiException("No start element found for process definition " + processDefinition.getId());
        }

        // Do not start a process instance if the process definition is suspended
        if (ProcessDefinitionUtil.isProcessDefinitionSuspended(processDefinition.getId())) {
            throw new ActivitiException("Cannot start process instance. Process definition " + processDefinition.getName() + " (id = " + processDefinition.getId() + ") is suspended");
        }

        ProcessEngineConfigurationImpl processEngineConfiguration = Context.getProcessEngineConfiguration();
        ExecutionEntityManager executionEntityManager = Context.getCommandContext().getExecutionEntityManager();
        ExpressionManager expressionManager = processEngineConfiguration.getExpressionManager();

        ExecutionEntity executionEntity = (ExecutionEntity) execution;
        CallActivity callActivity = (CallActivity) executionEntity.getCurrentFlowElement();

        String businessKey = null;

        if (!StringUtils.isEmpty(callActivity.getBusinessKey())) {
            Expression expression = expressionManager.createExpression(callActivity.getBusinessKey());
            businessKey = expression.getValue(execution).toString();

        } else if (callActivity.isInheritBusinessKey()) {
            ExecutionEntity processInstance = executionEntityManager.findById(execution.getProcessInstanceId());
            businessKey = processInstance.getBusinessKey();
        }

        ExecutionEntity subProcessInstance = Context.getCommandContext().getExecutionEntityManager().createSubprocessInstance(
                processDefinition, executionEntity, businessKey);
        Context.getCommandContext().getHistoryManager().recordSubProcessInstanceStart(executionEntity, subProcessInstance, initialFlowElement);

        // process template-defined data objects
        Map<String, Object> variables = processDataObjects(subProcess.getDataObjects());

        if (callActivity.isInheritVariables()) {
            Map<String, Object> executionVariables = execution.getVariables();
            for (Map.Entry<String, Object> entry : executionVariables.entrySet()) {
                variables.put(entry.getKey(), entry.getValue());
            }
        }
        Map<String, Object> variablesFromExtensionFile = calculateInboundVariables(execution, processDefinition);

        variables.putAll(variablesFromExtensionFile);
        variables = copyProcessVariables(execution, expressionManager, callActivity, variables);

        if (!variables.isEmpty()) {
            initializeVariables(subProcessInstance, variables);
        }

        // Create the first execution that will visit all the process definition elements
        ExecutionEntity subProcessInitialExecution = executionEntityManager.createChildExecution(subProcessInstance);
        subProcessInitialExecution.setCurrentFlowElement(initialFlowElement);

        Context.getAgenda().planContinueProcessOperation(subProcessInitialExecution);

        Context.getProcessEngineConfiguration().getEventDispatcher()
                .dispatchEvent(ActivitiEventBuilder.createProcessStartedEvent(subProcessInitialExecution, variables, false));
    }

    @Override
    public void completing(DelegateExecution execution, DelegateExecution subProcessInstance) throws Exception {

        Map<String, Object> outboundVariables = calculateOutBoundVariables(copyOutParameters(execution, subProcessInstance),
                subProcessInstance.getVariables());
        if (outboundVariables != null) {
            execution.setVariables(outboundVariables);
        }
    }

    @Override
    public void completed(DelegateExecution execution) throws Exception {
        // only control flow. no sub process instance data available
        leave(execution);
    }

    // Allow subclass to determine which version of a process to start.
    protected ProcessDefinition findProcessDefinition(String processDefinitionKey, String tenantId) {
        if (tenantId == null || ProcessEngineConfiguration.NO_TENANT_ID.equals(tenantId)) {
            return Context.getProcessEngineConfiguration().getDeploymentManager().findDeployedLatestProcessDefinitionByKey(processDefinitionKey);
        } else {
            return Context.getProcessEngineConfiguration().getDeploymentManager().findDeployedLatestProcessDefinitionByKeyAndTenantId(processDefinitionKey, tenantId);
        }
    }

    protected Map<String, Object> processDataObjects(Collection<ValuedDataObject> dataObjects) {
        Map<String, Object> variablesMap = new HashMap<String, Object>();
        // convert data objects to process variables
        if (dataObjects != null) {
            variablesMap = new HashMap<String, Object>(dataObjects.size());
            for (ValuedDataObject dataObject : dataObjects) {
                variablesMap.put(dataObject.getName(), dataObject.getValue());
            }
        }
        return variablesMap;
    }

    // Allow a subclass to override how variables are initialized.
    protected void initializeVariables(ExecutionEntity subProcessInstance, Map<String, Object> variables) {
        subProcessInstance.setVariables(variables);
    }

    public void setProcessDefinitonKey(String processDefinitonKey) {
        this.processDefinitonKey = processDefinitonKey;
    }

    public String getProcessDefinitonKey() {
        return processDefinitonKey;
    }

    protected Map<String, Object> calculateInboundVariables(DelegateExecution execution,
                                                            ProcessDefinition processDefinition) {
        return new HashMap<String, Object>();
    }

    protected Map<String, Object> calculateOutBoundVariables(DelegateExecution execution,
                                                             Map<String, Object> subProcessVariables) {
        return new HashMap<String, Object>();
    }

    protected Map<String, Object> copyProcessVariables(DelegateExecution execution, ExpressionManager expressionManager,
                                                       CallActivity callActivity, Map<String, Object> variables) {
        for (IOParameter ioParameter : callActivity.getInParameters()) {
            Object value = null;
            if (StringUtils.isNotEmpty(ioParameter.getSourceExpression())) {
                Expression expression = expressionManager.createExpression(ioParameter.getSourceExpression().trim());
                value = expression.getValue(execution);

            } else {
                value = execution.getVariable(ioParameter.getSource());
            }
            variables.put(ioParameter.getTarget(), value);
        }
        return variables;
    }

    protected DelegateExecution copyOutParameters(DelegateExecution execution, DelegateExecution subProcessInstance) {

        ExpressionManager expressionManager = Context.getProcessEngineConfiguration().getExpressionManager();
        ExecutionEntity executionEntity = (ExecutionEntity) execution;
        CallActivity callActivity = (CallActivity) executionEntity.getCurrentFlowElement();
        for (IOParameter ioParameter : callActivity.getOutParameters()) {
            Object value = null;
            if (StringUtils.isNotEmpty(ioParameter.getSourceExpression())) {
                Expression expression = expressionManager.createExpression(ioParameter.getSourceExpression().trim());
                value = expression.getValue(subProcessInstance);

            } else {
                value = subProcessInstance.getVariable(ioParameter.getSource());
            }
            execution.setVariable(ioParameter.getTarget(), value);
        }
        return execution;
    }

}
